// this is based on https://github.com/jakesgordon/javascript-state-machine, but enhanced to support
// multiple possible transitions to and from various stated, guarded by the onBefore callbacks
// copy right notice...
//Copyright (c) 2012 Jake Gordon and contributors
//
//Permission is hereby granted, free of charge, to any person obtaining a copy
//of this software and associated documentation files (the "Software"), to deal
//in the Software without restriction, including without limitation the rights
//to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//copies of the Software, and to permit persons to whom the Software is
//furnished to do so, subject to the following conditions:
//
//The above copyright notice and this permission notice shall be included in all
//copies or substantial portions of the Software.
//
//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
//SOFTWARE.
(function(window) {

    var StateMachine = {
        //---------------------------------------------------------------------------

        VERSION: "2.2.0",
        //---------------------------------------------------------------------------

        Result: {
            SUCCEEDED: 1, // the event transitioned successfully from one state to another
            NOTRANSITION: 2, // the event was successfull but no state transition was necessary
            CANCELLED: 3, // the event was cancelled by the caller in a beforeEvent callback
            ASYNC: 4 // the event is asynchronous and the caller is in control of when the transition occurs
        },
        Error: {
            INVALID_TRANSITION: 100, // caller tried to fire an event that was innapropriate in the current state
            PENDING_TRANSITION: 200, // caller tried to fire an event while an async transition was still pending
            INVALID_CALLBACK: 300, // caller provided callback function threw an exception
            MULTIPLE_TRANSITIONS: 400 // provided multiple transitions
        },
        WILDCARD: '*',
        ASYNC: 'async',
        //---------------------------------------------------------------------------

        create: function(cfg, target) {

            var initial = (typeof cfg.initial == 'string') ? {state: cfg.initial} : cfg.initial; // allow for a simple string, or an object with { state: 'foo', event: 'setup', defer: true|false }
            var fsm = target || cfg.target || {};
            var events = cfg.events || [];
            var callbacks = cfg.callbacks || {};
            var map = {};

            var add = function(e) {
                var from = (e.from instanceof Array) ? e.from : (e.from ? [e.from] : [StateMachine.WILDCARD]); // allow 'wildcard' transition if 'from' is not specified
                var to = (e.to instanceof Array) ? e.to : [e.to];
                map[e.name] = map[e.name] || {};
                for (var n = 0; n < from.length; n++)
                {
                    map[e.name][from[n]] = {};
                    map[e.name][from[n]].to = to || [from[n]];
                }
            };
            if (initial) {
                initial.event = initial.event || 'startup';
                add({name: initial.event, from: 'none', to: initial.state});
            }

            for (var n = 0; n < events.length; n++)
                add(events[n]);

            for (var name in map) {
                if (map.hasOwnProperty(name))
                    fsm[name] = StateMachine.buildEvent(name, map[name]);
            }

            for (var name in callbacks) {
                if (callbacks.hasOwnProperty(name))
                    fsm[name] = callbacks[name];
            }

            fsm.current = 'none';
            fsm.is = function(state) {
                return this.current == state;
            };
            fsm.can = function(event) {
                return !this.transition && (map[event][this.current] || map[event][StateMachine.WILDCARD]);
            };
            fsm.cannot = function(event) {
                return !this.can(event);
            };
            fsm.error = cfg.error || function(name, from, to, args, error, msg, e) {
                throw e || msg;
            }; // default behavior when something unexpected happens is to throw an exception, but caller can override this behavior if desired (see github issue #3 and #17)

            if (initial && !initial.defer)
                fsm[initial.event]();

            return fsm;

        },
        //===========================================================================

        doCallback: function(fsm, func, name, from, to, args) {
            if (func) {
                try {
                    return func.apply(fsm, [name, from, to].concat(args));
                }
                catch (e) {
                    return fsm.error(name, from, to, args, StateMachine.Error.INVALID_CALLBACK, "an exception occurred in a caller-provided callback function", e);
                }
            }
        },
        beforeEvent: function(fsm, name, from, to, args) {
            return StateMachine.doCallback(fsm, fsm['onbefore' + name], name, from, to, args);
        },
        afterEvent: function(fsm, name, from, to, args) {
            return StateMachine.doCallback(fsm, fsm['onafter' + name] || fsm['on' + name], name, from, to, args);
        },
        leaveState: function(fsm, name, from, to, args) {
            return StateMachine.doCallback(fsm, fsm['onleave' + from], name, from, to, args);
        },
        enterState: function(fsm, name, from, to, args) {
            return StateMachine.doCallback(fsm, fsm['onenter' + to] || fsm['on' + to], name, from, to, args);
        },
        changeState: function(fsm, name, from, to, args) {
            return StateMachine.doCallback(fsm, fsm['onchangestate'], name, from, to, args);
        },
        buildEvent: function(name, map) {
            return function() {

                var from = this.current;
                var toArray = map[from].to || [map[StateMachine.WILDCARD]] || [from];
                var to;
                var args = Array.prototype.slice.call(arguments); // turn arguments into pure array

                if (this.transition)
                    return this.error(name, from, toArray[0], args, StateMachine.Error.PENDING_TRANSITION, "event " + name + " inappropriate because previous transition did not complete");

                if (this.cannot(name))
                    return this.error(name, from, toArray[0], args, StateMachine.Error.INVALID_TRANSITION, "event " + name + " inappropriate in current state " + this.current);

                for (var i = 0; i < toArray.length; i++)
                {
                    if (!(false === StateMachine.beforeEvent(this, name, from, toArray[i], args)))
                    {
                        if (to)
                        {
                            // already defined the to!
                            return this.error(name, from, toArray[i], args, StateMachine.Error.MULTIPLE_TRANSITIONS);
                        }
                        to = toArray[i];
                    }
                }
                if (!to)
                    return StateMachine.CANCELLED;

                if (from === to) {
                    StateMachine.afterEvent(this, name, from, to, args);
                    StateMachine.changeState(this, name, from, to, args);
                    return StateMachine.NOTRANSITION;
                }

                // prepare a transition method for use EITHER lower down, or by caller if they want an async transition (indicated by an ASYNC return value from leaveState)
                var fsm = this;
                this.transition = function() {
                    fsm.transition = null; // this method should only ever be called once
                    fsm.current = to;
                    StateMachine.enterState(fsm, name, from, to, args);
                    StateMachine.afterEvent(fsm, name, from, to, args);
                    StateMachine.changeState(fsm, name, from, to, args);
                };
                this.transition.cancel = function() { // provide a way for caller to cancel async transition if desired (issue #22)
                    fsm.transition = null;
                    StateMachine.afterEvent(fsm, name, from, to, args);
                };

                var leave = StateMachine.leaveState(this, name, from, to, args);
                if (false === leave) {
                    this.transition = null;
                    return StateMachine.CANCELLED;
                }
                else if ("async" === leave) {
                    return StateMachine.ASYNC;
                }
                else {
                    if (this.transition)
                        this.transition(); // in case user manually called transition() but forgot to return ASYNC
                    return StateMachine.SUCCEEDED;
                }

            };
        }

    }; // StateMachine

    //===========================================================================

    if ("function" === typeof define) {
        define(function(require) {
            return StateMachine;
        });
    }
    else {
        window.StateMachine = StateMachine;
    }

}(this));
